﻿/*This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>
	*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;


namespace ZipLib
{
    /// <summary>
    /// Zip file
    /// </summary>
    public class Zip : IEnumerable<ZipEntryHeader>, IDisposable
    {
        private ZipEndOfCentralDirectory Endheader { get; set; }
        private List<ZipEntryHeader> EntryHeaders { get; set; }
        private Stream _filestream;

        /// <summary>
        /// Read Zip file from given path
        /// </summary>
        /// <param name="filename">File path</param>
        public Zip(string filename) : this(filename, false)
        {
        }

        /// <summary>
        /// Read Zip file from give path with certain read-write permission
        /// </summary>
        /// <param name="filename">File path</param>
        /// <param name="write">true to have write permission; otherwise only have read permission</param>
        public Zip(string filename, bool write)
            : this(new FileStream(filename, write ? FileMode.OpenOrCreate : FileMode.Open))
        {
        }

        /// <summary>
        /// Initialize a new instance of zip file
        /// </summary>
        public Zip()
        {
            _filestream = null;
            Endheader = new ZipEndOfCentralDirectory();
            EntryHeaders = new List<ZipEntryHeader>();
        }

        /// <summary>
        /// Initialize a new instance of zip file with given stream
        /// </summary>
        /// <param name="s">Stream containing zip file</param>
        public Zip(Stream s) : this(new BinaryReader(s))
        {
        }

        /// <summary>
        /// Initialize a new instance of zip file with given binary reader
        /// </summary>
        /// <param name="r">Binary reader containing zip stream</param>
        public Zip(BinaryReader r)
        {
            if (!r.BaseStream.CanRead) throw new ZipException("Stream is not readable");
            _filestream = r.BaseStream;
            r.BaseStream.Position = EndOfDictoryFinder(r);
            Endheader = new ZipEndOfCentralDirectory(r);
            EntryHeaders = new List<ZipEntryHeader>(Endheader.TotalEntries);
            r.BaseStream.Position = Endheader.CentralDirectoryOffset;
            if (!NewRead(r)) // Using old ways
            {
                OldReading(r);
            }
        }

        /// <summary>
        /// Add entry file to zip file
        /// </summary>
        /// <param name="filename">FileName of the entry. Use "/" to separate directory</param>
        /// <param name="source">Source stream</param>
        public void AddEntry(string filename, Stream source)
        {
            EntryHeaders.Add(new ZipCentralDirectoryHeader(source, filename));
        }

        /// <summary>
        /// Add an empty entry file
        /// </summary>
        /// <param name="filename">FileName of the entry. Use "/" to separate directory</param>
        public void AddEntry(string filename)
        {
            AddEntry(filename, null);
        }

        /// <summary>
        /// Save the file to where it comes from
        /// </summary>
        public void Save()
        {
            bool usingTemp = false;
            if (usingTemp)
            {
                string strTemp = Path.GetTempFileName();
                using (FileStream fs = new FileStream(strTemp, FileMode.Create))
                {
                    SaveAs(fs);
                    fs.Position = 0;
                    _filestream.SetLength(0);
                    fs.CopyTo(_filestream);
                }

                File.Delete(strTemp); // Delete the temp file
            }
            else // when the file is much more smaller than the memory
            {
                _filestream.SetLength(0);
                this.SaveAs((FileStream)_filestream);
            }
        }

        /// <summary>
        /// Save zip file to certain file path
        /// </summary>
        /// <param name="filename">File path to save</param>
        public void SaveAs(string filename)
        {
            SaveAs(new FileStream(filename, FileMode.Create));
        }

        /// <summary>
        /// Save zip file to file stream
        /// </summary>
        /// <param name="fs">File stream to be written</param>
        public void SaveAs(FileStream fs)
        {
            //if (fs == _filestream) throw new ZipException("Cannot write to itself using save as");
            if (_disposed) throw new ZipException("FileStream already disposed");
            BinaryWriter w = new BinaryWriter(fs);
            foreach (ZipCentralDirectoryHeader entry in EntryHeaders)
            {
                // Unless I found a way to all the original function, I will put the method here.
                entry.LocalHeaderOffset = (int) w.BaseStream.Position;
                w.Write(0x04034B50);
                w.Write(entry.VersionNeeded);
                w.Write(entry.Flags);
                w.Write(entry.Compression);
                w.Write(entry.ModTime);
                w.Write(entry.ModDate);
                w.Write(entry.Crc32);
                w.Write(entry.CompressedSize);
                w.Write(entry.UncompressedSize);
                w.Write(entry.FileNameLength);
                w.Write(entry.ExtraFieldLength);
                // I want to use the central one, but I found 7-Zip just ignore them here
                w.Write(Encoding.ASCII.GetBytes(entry.FileName));
                w.Write(entry.ExtraFiled); // Same issue in 7-Zip
                entry.StreamPosition = w.BaseStream.Position;


                w.Write(entry.ResourceStream != null
                    ? ((MemoryStream) entry.ResourceStream).ToArray()
                    : ((MemoryStream) GetCompressedStream(entry)).ToArray());
            }

            Endheader.CentralDirectoryOffset = (uint) w.BaseStream.Position;

            foreach (ZipCentralDirectoryHeader entry in EntryHeaders)
            {
                entry.Parse(w);
            }

            // Write the end of central directory header
            Endheader.CentralDirectorySize = (int) (w.BaseStream.Position - Endheader.CentralDirectoryOffset);
            Endheader.TotalEntries = (short) EntryHeaders.Count;
            Endheader.DiskEntries = (short) EntryHeaders.Count;
            Endheader.Parse(w);

            w.Flush();
        }

        private long EndOfDictoryFinder(BinaryReader r)
        {
            r.BaseStream.Position = 0;
            if (r.ReadInt32() != 0x04034B50) throw new ZipException("Unknown file opened.");
            long position = r.BaseStream.Length;
            r.BaseStream.Position = position - 2;
            if (r.ReadUInt16() == 0x0000U)
                return r.BaseStream.Length - 22; // No comment
            if (r.BaseStream.Length < 240)
            {
                r.BaseStream.Position = 0;
                if (r.ReadByte() == 0x50 && r.ReadByte() == 0x4B && r.ReadByte() == 0x05 && r.ReadByte() == 0x06)
                {
                    return r.BaseStream.Position - 4;
                }
            }

            // Buffer length 0XFF, read back from the comment
            for (int i = 0; i < 0xFFFF/0XFF; i += 0xFF)
            {
                r.BaseStream.Position = r.BaseStream.Length - (i + 1)*0xFF;
                byte[] data = r.ReadBytes(0xFF);
                for (int n = 0; n < 0xFF - 4; n++)
                {
                    if (data[n] == 0x50 && data[n + 1] == 0x4B && data[n + 2] == 0x05 && data[n + 3] == 0x06)
                    {
                        return r.BaseStream.Length - (i + 1)*0xFF + n;
                    }
                }
            }
            throw new ZipException("End Of Directory Not Found");
        }

        private bool NewRead(BinaryReader r)
        {
            if (r.ReadInt32() != 0x02014B50) return false;
            r.BaseStream.Position -= 4;
            for (int i = 0; i < Endheader.TotalEntries; i++)
            {
                EntryHeaders.Add(new ZipCentralDirectoryHeader(r));
            }
            return true;
        }

        private void OldReading(BinaryReader r)
        {
            r.BaseStream.Position = 0;
            int signature = r.ReadInt32();
            EntryHeaders = new List<ZipEntryHeader>();
            while (signature == 0x04034b50)
            {
                r.BaseStream.Position -= 4;
                ZipEntryHeader entryHeader = new ZipEntryHeader(r);

                if (entryHeader.Flags == 0x0808) // I guess I need to search for signature 0x08074b50
                {
                    byte b = r.ReadByte();
                    while (true)
                    {
                        if (b == 0x50 && r.ReadByte() == 0x4B && r.ReadByte() == 0x07 && r.ReadByte() == 0x08)
                        {
                            entryHeader.Crc32 = r.ReadUInt32();
                            entryHeader.CompressedSize = r.ReadUInt32();
                            entryHeader.UncompressedSize = r.ReadUInt32();

                            EntryHeaders.Add(entryHeader);
                            break;
                        }
                        b = r.ReadByte();
                    }
                }
                else
                {
                    EntryHeaders.Add(entryHeader);
                    r.BaseStream.Position += entryHeader.CompressedSize;
                }
                signature = r.ReadInt32();
            }
        }

        private Stream GetCompressedStream(ZipEntryHeader header)
        {
            _filestream.Position = header.StreamPosition;
            BinaryReader r = new BinaryReader(_filestream);
            byte[] bytes = r.ReadBytes((int) header.CompressedSize);
            MemoryStream compressedStream = new MemoryStream(bytes) {Position = 0};
            return compressedStream;
        }

        /// <summary>
        /// Get uncompressed stream from given entry
        /// </summary>
        /// <param name="entry">File entry</param>
        /// <returns></returns>
        public Stream GetUncompressedStream(ZipEntryHeader entry)
        {
            Stream compressedStream = GetCompressedStream(entry);
            if (entry.UncompressedSize == 0 || entry.CompressedSize == entry.UncompressedSize) return compressedStream;
            Stream result = null;
            try
            {
                Crc32 crc32 = new Crc32();
                uint hash = crc32.CheckCrc32(compressedStream, out result);
                if (result.Length != entry.UncompressedSize || hash != entry.Crc32)
                    throw new ZipException("File Content Corrupted");
                result.Position = 0;
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }

            return result;
        }

        #region List Interface

        public ZipEntryHeader this[int index]
        {
            get { return EntryHeaders[index]; }
            set { EntryHeaders[index] = value; }
        }

        public IEnumerator<ZipEntryHeader> GetEnumerator()
        {
            return EntryHeaders.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        #region IDisposable Interface

        public void Dispose()
        {
            if(!_disposed)
            {
                Dispose(true);
            }
        }
        private bool _disposed = false;

        protected void Dispose(bool disposing)
        {
            if(!_disposed)
            {
                if (disposing)
                    if (_filestream != null)
                    {
                        _filestream.Flush();
                        _filestream.Dispose();
                    }
                _filestream = null;
                _disposed = true;
            }
        }

        #endregion
    }
}